﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using MathExpression;
using EMessageType = SimplexSolver.MessageTransferNeededEventArgs.EMessageType;

namespace SimplexSolver
{
    /// <summary>
    /// Solve LPTask where all values (target, restrictions, etc.) represented as Scalars only
    /// </summary>
    internal class ScalarSimplexSolver
    {
        #region Messages

        private const string FREE_VALUE_MUST_BE_NON_NEGATIVE =
            "Столбец свободных членов не может иметь отрицательные элементы";

        private const string FAILED_SOLVE_SECOND_BASIS = "Не удалось решить задачу методом искусственного базиса";

        private const string INCORRECT_INDEX = "В заголовок симплексной таблицы записан некорректный индекс";

        #endregion

        public event EventHandler<MessageTransferNeededEventArgs> MessageTransferNeeded;

        private bool _isPossibleToSolve;

        /// <summary>
        /// Solve LPTask in scalars
        /// </summary>
        /// <param name="task">Task for solving</param>
        /// <returns>Solution of solved LPTask or NULL if LPTask can not be solved</returns>
        /// <exception cref="ArgumentException">Thrown when LPTask contains not Scalars items</exception>
        public ISolution Solve(ILPTask task)
        {
            if (!CheckValidType(task))
            {
                const string message = "Задача содержит элемент неверного типа. Требуется тип: Scalar";
                OnMessageTransferNeeded(message, EMessageType.Warning);
                return null;
            }
            SimplexTable table = SimplexTableBuilder.Create(task);
            if (HasSecondBasis(table))
            {
                SimplexTable secondBasisTable = SolveSecondBasisTask(table);
                if (HasSecondBasis(secondBasisTable))
                {
                    throw new Exception(FAILED_SOLVE_SECOND_BASIS);
                }
                table = SimplexTableBuilder.CreateFromSecondBasis(table.TargetRow, secondBasisTable);
            }
            table = SolveSimplexTable(table, task.Target.Direction);

            IMathExpression[] solutionVector = table.GetSolutionVector(task.Size);
            IMathExpression value = task.Target.GetValueOf(solutionVector);
            ISolution solution;
            if (IsNeedToFindSecondSolve(table))
            {
                DoSimplexStep(task.Target.Direction, table);
                IMathExpression[] secondSolution = table.GetSolutionVector(task.Size);
                if (solutionVector.SequenceEqual(secondSolution))
                {
                    solution = new SingleSolution(value, solutionVector);
                }
                else
                {
                    solution = new DoubleSolution(value, solutionVector, secondSolution);
                }
            }
            else
            {
                solution = new SingleSolution(value, solutionVector);
            }
            return solution;
        }

        /// <summary>
        /// Check type of target and restrictions items
        /// </summary>
        /// <param name="task">Task for checking</param>
        /// <returns>True if all items are type of Scalar, false in other case</returns>
        private bool CheckValidType(ILPTask task)
        {
            TargetFunction target = task.Target;
            if (target.Coefs.Any(coef => !(coef.Data is ScalarData)))
            {
                return false;
            }
            ReadOnlyCollection<Restriction> restrictions = task.Restrictions;
            foreach (Restriction restriction in restrictions)
            {
                if (!(restriction.Limit.Data is ScalarData))
                {
                    return false;
                }
                if (restriction.DataRow.Any(item => !(item.Data is ScalarData)))
                {
                    return false;
                }
            }
            return true;
        }

        private SimplexTable SolveSimplexTable(SimplexTable table, TargetFunction.EDirection direction)
        {
            SimplexTable solvedTable = table.Clone();
            bool isOptim = CheckSolvingConditions(solvedTable, direction);
            while (!isOptim)
            {
                DoSimplexStep(direction, solvedTable);
                isOptim = CheckSolvingConditions(solvedTable, direction);
            }
            return solvedTable;
        }

        private void DoSimplexStep(TargetFunction.EDirection direction, SimplexTable solvedTable)
        {
            int columnIndex, rowIndex;
            _isPossibleToSolve = TryFindItemForCalculating(solvedTable, out rowIndex, out columnIndex, direction);
            if (_isPossibleToSolve)
            {
                solvedTable.Recalculate(rowIndex + 1, columnIndex + 1);
            }
        }

        private bool IsNeedToFindSecondSolve(SimplexTable table)
        {
            IMathExpression[] row = table.TargetRow;
            foreach (IMathExpression expression in row)
            {
                var data = expression.Data as ScalarData;
                if (data != null && Math.Abs(data.Value - 0.0) < double.Epsilon)
                {
                    return true;
                }
            }
            return false;
        }

        private SimplexTable SolveSecondBasisTask(SimplexTable table)
        {
            SimplexTable newTable = SimplexTableBuilder.CreateSecondBasis(table);
            return SolveSimplexTable(newTable, TargetFunction.EDirection.Min);
        }

        private bool CheckSolvingConditions(SimplexTable table, TargetFunction.EDirection direction)
        {
            if (!CheckCorrectFreeValues(table.FreeValueColumn))
            {
                OnMessageTransferNeeded(FREE_VALUE_MUST_BE_NON_NEGATIVE, EMessageType.Warning);
                return false;
            }
            bool isOptim = CheckOptimTargetRow(table.TargetRow, direction);
            return isOptim;
        }

        private bool TryFindItemForCalculating(SimplexTable table, out int rowIndex, out int columnIndex,
                                               TargetFunction.EDirection direction)
        {
            IMathExpression[] target = table.TargetRow;
            columnIndex = GetDirectColumnIndexForCalculating(target, direction);
            rowIndex = GetDirectRowIndexForCalculating(table, columnIndex);
            return columnIndex >= 0 && rowIndex >= 0;
        }

        private int GetDirectColumnIndexForCalculating(IList<IMathExpression> values,
                                                       TargetFunction.EDirection direction)
        {
            int index;
            if (direction == TargetFunction.EDirection.Min)
            {
                index = GetMaxElementIndex(values);
            }
            else
            {
                index = GetMinElementIndex(values);
            }
            return index;
        }

        private int GetDirectRowIndexForCalculating(SimplexTable table, int columnIndex)
        {
            int index = -1;
            int count = table.RowsCount - 1;
            double min = double.MaxValue;
            for (int i = 0; i < count; i++)
            {
                var scalarData = table.RestrictionMatrix[i][columnIndex].Data as ScalarData;
                if (scalarData != null)
                {
                    double value = scalarData.Value;
                    if (value > 0.0)
                    {
                        var data = table.FreeValueColumn[i].Data as ScalarData;
                        if (data != null)
                        {
                            double freeValue = data.Value;
                            if (freeValue < 0.0)
                            {
                                throw new Exception(FREE_VALUE_MUST_BE_NON_NEGATIVE);
                            }
                            value = freeValue/value;
                        }
                        if (value < min)
                        {
                            min = value;
                            index = i;
                        }
                    }
                }
            }
            return index;
        }

        private int GetMinElementIndex(IList<IMathExpression> values)
        {
            double min = double.MaxValue;
            int index = -1;
            int count = values.Count;
            for (int i = 0; i < count; i++)
            {
                var scalarData = values[i].Data as ScalarData;
                if (scalarData != null)
                {
                    double value = scalarData.Value;
                    if (value < min)
                    {
                        min = value;
                        index = i;
                    }
                }
            }
            return index;
        }

        private int GetMaxElementIndex(IList<IMathExpression> values)
        {
            double max = double.MinValue;
            int index = -1;
            int count = values.Count;
            for (int i = 0; i < count; i++)
            {
                var scalarData = values[i].Data as ScalarData;
                if (scalarData != null)
                {
                    double value = scalarData.Value;
                    if (value > max)
                    {
                        max = value;
                        index = i;
                    }
                }
            }
            return index;
        }

        private bool CheckCorrectFreeValues(IEnumerable<IMathExpression> freeValues)
        {
            return freeValues.All(value => (value.Data as ScalarData).Value >= 0.0);
        }

        private bool CheckOptimTargetRow(IEnumerable<IMathExpression> tableValues, TargetFunction.EDirection direction)
        {
            foreach (IMathExpression value in tableValues)
            {
                if (direction == TargetFunction.EDirection.Min &&
                    (value.Data as ScalarData).Value > 0.0)
                {
                    return false;
                }
                if (direction == TargetFunction.EDirection.Max &&
                    (value.Data as ScalarData).Value < 0.0)
                {
                    return false;
                }
            }
            return true;
        }

        private bool HasSecondBasis(SimplexTable table)
        {
            string[] rowHeaders = table.RowHeaders;
            return rowHeaders.Any(header => header[0] == 'z');
        }

        private void OnMessageTransferNeeded(string message, EMessageType type)
        {
            if (MessageTransferNeeded != null)
            {
                var e = new MessageTransferNeededEventArgs(message, type);
                MessageTransferNeeded(this, e);
            }
        }
    }
}